android create project --target android-10 --name todo \\
 --path ~/android/todo --activity ToDo --package com.example.todo

---

public static final String AUTHORITY = "com.example.todo.tododatabaseprovider"; public static final String TODO_BASE_PATH = "todo";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
   + "/" + TODO_BASE_PATH);

---

<provider android:authorities="com.example.todo.tododatabaseprovider"
         android:multiprocess="true"
         android:name="com.example.todo.TodoDatabaseProvider">
</provider>

---

private static class TodoDBOpenHelper extends SQLiteOpenHelper {

 TodoDBOpenHelper(Context c) {
   super(c, DB_NAME, null, DB_VERSION);
 }

 @Override
 public void onCreate(SQLiteDatabase db) {
   db.execSQL("CREATE TABLE " + TASK_TABLE_NAME + " ("
              + ID + " INTEGER PRIMARY KEY,"
              + COL_TASK + " TEXT,"
              + COL_DUEDATE + " DATE,"
              + COL_CREATEDATE + " INTEGER,"
              + COL_CATEGORY_LINK + " INTEGER REFERENCES "
                                      + CATEGORY_TABLE + "("
                                      + COL_CATEGORY_ID + ")"
              + ");");
   db.execSQL("CREATE TABLE " + CATEGORY_TABLE_NAME + " ("
              + ID + " INTEGER PRIMARY KEY,"
              + COL_CATEGORY + " TEXT"
              + ");");
 db.execSQL("INSERT INTO " + CATEGORY_TABLE + " VALUES(1, 'work');");
 db.execSQL("INSERT INTO " + CATEGORY_TABLE + " VALUES(2, 'personal');");
 }
}

---

private static final int DB_VERSION = 1;

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 db.execSQL("DROP TABLE IF EXISTS " + TASK_TABLE);
 db.execSQL("DROP TABLE IF EXISTS " + CATEGORY_TABLE);
 onCreate(db);

}

---

private static final int TODOS = 100;
private static final int TODO_ID = 101;
private static final UriMatcher matcher =
   new UriMatcher(UriMatcher.NO_MATCH);
static {
 matcher.addURI(AUTHORITY, TODO_BASE_PATH, TODOS);
 matcher.addURI(AUTHORITY, TODO_BASE_PATH + "/#", TODO_ID);
}

@Override
public boolean onCreate() {
 db = new TodoDBOpenHelper(getContext());
 return true;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection,
                   String[] selectionArgs, String sortOrder) {
 SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
 builder.setTables(TASK_TABLE);

 int uriType = matcher.match(uri);
 switch (uriType) {
   case TODO_ID:
     builder.appendWhere(ID + "=" + uri.getLastPathSegment());
     break;
   case TODOS:
     break;
   default:
     throw new IllegalArgumentException("Unknown URI" + uri);
 }

 Cursor cursor = builder.query(db.getReadableDatabase(),
     projection, selection, selectionArgs, null, null, sortOrder);
 cursor.setNotificationUri(getContext().getContentResolver(), uri);
 return cursor;
}

---

@Override
public Uri insert(Uri uri, ContentValues values) {
 int uriType = matcher.match(uri);
 if (uriType != TODOS) {
   throw new IllegalArgumentException("Invalid URI for insert");
 }
 SQLiteDatabase sqlDB = db.getWritableDatabase();
 long newID = sqlDB.insert(TASK_TABLE, COL_TASK, values);
 if (newID > 0) {
   Uri newUri = ContentUris.withAppendedId(uri, newID);
   getContext().getContentResolver().notifyChange(uri, null);
   return newUri;
 } else {
   throw new SQLException("Failed to insert row into " + uri
       + "result " + newID);
 }
}

@Override
public int update(Uri uri, ContentValues values, String selection,
   String[] selectionArgs) {
 int uriType = matcher.match(uri);
 if (uriType != TODO_ID) {
   throw new IllegalArgumentException("Invalid URI for update");
 }
 long id = ContentUris.parseId(uri);
 String where = ID + "='" + id + "'";
 SQLiteDatabase sqlDB = db.getWritableDatabase();
 int result = sqlDB.update(TASK_TABLE, values, where, null);
 return result;
}

---

private static final String[] TODO_PROJECTION = new String[] {
 TodoDatabaseProvider.ID,
 TodoDatabaseProvider.COL_TASK,
 TodoDatabaseProvider.COL_DUEDATE
};

private void populateList() {
 Cursor cursor = managedQuery(TodoDatabaseProvider.CONTENT_URI,
     TODO_PROJECTION, null, null, null);
 String[] colNames = { TodoDatabaseProvider.COL_TASK,
                       TodoDatabaseProvider.COL_DUEDATE };
 int[] viewIDs = { R.id.taskname, R.id.duedate };
 SimpleCursorAdapter adapter = new SimpleCursorAdapter(
     this,
     R.layout.todo_item,
     cursor,
     colNames,
     viewIDs
 ); setListAdapter(adapter);
}

---

@Override
 public boolean onCreateOptionsMenu(Menu menu) {
   MenuInflater inflater = getMenuInflater();
   inflater.inflate(R.menu.todo_options_menu, menu);
   return super.onCreateOptionsMenu(menu);
}

---

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:id="@+id/menu_add"
       android:title="@string/menu_add"
       android:alphabeticShortcut='a' />
</menu>

---

@Override
public boolean onContextItemSelected(MenuItem item) {
 AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
 switch(item.getItemId()) {
   case R.id.menu_add:
     Intent i = new Intent(Intent.ACTION_INSERT);
     i.setClass(this, TodoEditor.class);
     startActivity(i);
     return true;
   default:
     return super.onOptionsItemSelected(item);
 }
}

---

@Override public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 final Intent intent = getIntent();
 final String action = intent.getAction();
 if (Intent.ACTION_INSERT.equals(action)) {
   state = STATE_INSERT;
   uri = getContentResolver().insert(TodoDatabaseProvider.CONTENT_URI, null);
   if (uri == null) {
     Log.e(TAG, "Failed to insert new todo into "
           + TodoDatabaseProvider.CONTENT_URI);
     finish();
     return;
   }
   setResult(RESULT_OK, (new Intent()).setAction(uri.toString()));
 } else {
   Log.e(TAG, "Unrecognised action " + action + ", exiting");
   finish();
   return;
 }
 cursor = managedQuery(uri, TODO_PROJECTION, null, null, null);
 setContentView(R.layout.todo_editor);
 text = (EditText) findViewById(R.id.todo);
 date = (EditText) findViewById(R.id.duedate);
 if (savedInstanceState != null) {
   initialTodo = savedInstanceState.getString(INITIAL_TODO);
 }
}

---

private final void updateTodo(String task, String duedate) {
 ContentValues values = new ContentValues();
 if (state == STATE_INSERT) {
   values.put(TodoDatabaseProvider.COL_CREATEDATE, System.currentTimeMillis());
   values.put(TodoDatabaseProvider.COL_TASK, task);
   values.put(TodoDatabaseProvider.COL_DUEDATE, duedate);
 }
 getContentResolver().update(uri, values, null, null);
}

---

String joinQuery = "select " + TASK_TABLE + "." + ID + ", " + TASK_TABLE + "."
                  + COL_TASK + ", " + TASK_TABLE + "." + COL_DUEDATE + ", "
                  + CATEGORY_TABLE + "."  + COL_CATEGORY
                  + " from " + TASK_TABLE + " join " + CATEGORY_TABLE
                  + " on " + TASK_TABLE + "." + COL_CATEGORY_LINK + "="
                  + CATEGORY_TABLE + "." + ID;

private void populateList() {
 Cursor cursor = TodoDatabaseProvider.getReadableDb().rawQuery(joinQuery, null);
 String[] colNames = { TodoDatabaseProvider.COL_TASK,
                       TodoDatabaseProvider.COL_DUEDATE,
                       TodoDatabaseProvider.COL_CATEGORY };
 int[] viewIDs = { R.id.taskname, R.id.duedate, R.id.category };
 // rest is the same

---

private static final String[] CATEGORY_PROJECTION = new String[] {
   TodoDatabaseProvider.COL_CATEGORY_ID,
   TodoDatabaseProvider.COL_CATEGORY
};

public void onCreate(Bundle savedInstanceState) {
 // method the same to here.
 setContentView(R.layout.todo_editor);
 text     = (EditText) findViewById(R.id.todo);
 date     = (EditText) findViewById(R.id.duedate);
 Spinner category = (Spinner) findViewById(R.id.category);
 category.setOnItemSelectedListener(new CategoryItemSelectedListener());

 Cursor categoryCursor = managedQuery(TodoDatabaseProvider.CONTENT_URI,
               CATEGORY_PROJECTION, null, null, null);

 SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
     android.R.layout.simple_spinner_item,
     cursor,
     new String[] {TodoDatabaseProvider.COL_CATEGORY},
     new int[] {android.R.id.text1} );
 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
 category.setAdapter(adapter);
}

---

@Override public boolean onContextItemSelected(MenuItem item) {
 AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
 switch(item.getItemId()) {
   case R.id.menu_edit:
     Intent i = new Intent(Intent.ACTION_EDIT);
     i.setClass(this, TodoEditor.class);
     i.putExtra("id", info.id);
     startActivity(i);
     return true;
   case R.id.menu_delete:
     deleteItem(info.id);
     return true;
   default:
     return super.onContextItemSelected(item);
 }
}

---

private void deleteItem(long id) {
 String where = TodoDatabaseProvider.ID + "=" + id;
 getContentResolver().delete(TodoDatabaseProvider.CONTENT_URI,
     where, null);
 populateList();
}

---

if (Intent.ACTION_INSERT.equals(action)) {
 // this is the same as before
} else if (Intent.ACTION_EDIT.equals(action)) {
 state = STATE_EDIT;
 long itemId = intent.getLongExtra("id", 0);
 uri = ContentUris.withAppendedId(TodoDatabaseProvider.CONTENT_URI,
         itemId);
}

cursor = managedQuery(uri, TODO_PROJECTION, null, null, null);
// ... set up XML content view as before ...

if (state == STATE_EDIT) {
 cursor.moveToFirst();
 text.setText(cursor.getString(
              cursor.getColumnIndex(TodoDatabaseProvider.COL_TASK)),
              TextView.BufferType.EDITABLE);
 date.setText(cursor.getString(
              cursor.getColumnIndex(TodoDatabaseProvider.COL_DUEDATE)),
              TextView.BufferType.EDITABLE);
 // Subtract 1 because SQL IDs start at 1, and spinner array starts at 0
 category.setSelection(cursor.getInt(
              cursor.getColumnIndex(TodoDatabaseProvider.COL_CATEGORY_LINK))
              - 1);
}

---

if (state == STATE_INSERT || state == STATE_EDIT) {
